package com.answer.controller;

import cn.hutool.json.JSONUtil;
import com.answer.annotation.AuthCheck;
import com.answer.common.BaseResponse;
import com.answer.common.DeleteRequest;
import com.answer.common.ErrorCode;
import com.answer.constant.UserConstant;
import com.answer.entity.App;
import com.answer.entity.Question;
import com.answer.entity.User;
import com.answer.exception.BusinessException;
import com.answer.model.dto.question.*;
import com.answer.model.enums.AppTypeEnum;
import com.answer.model.vo.QuestionVO;
import com.answer.service.AppService;
import com.answer.service.QuestionService;
import com.answer.service.UserService;
import com.answer.service.manager.AiManager;
import com.answer.utils.ResultUtils;
import com.answer.utils.ThrowUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Objects;

/**
 * 题目接口
 */
@RestController
@RequestMapping("/question")
@Slf4j
public class QuestionController {

    @Resource
    private QuestionService questionService;

    @Resource
    private UserService userService;

    @Resource
    private AppService appService;

    @Resource
    private AiManager aiManager;

    /**
     * 创建题目
     *
     * @param questionAddRequest 包含要添加问题的信息的请求对象
     * @param request            HTTP请求对象，用于获取当前登录用户
     * @return 返回一个包含新创建问题的ID的响应对象
     */
    @PostMapping("/add")
    public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {
        // 校验传入的请求参数是否为空
        ThrowUtils.throwIf(questionAddRequest == null, ErrorCode.PARAMS_ERROR);
        // 初始化Question实体，并从请求对象中复制属性
        Question question = new Question();
        BeanUtils.copyProperties(questionAddRequest, question);
        // 处理问题内容列表，将其转换为JSON字符串，以便存储在数据库中
        List<QuestionContentDTO> questionContentDTO = questionAddRequest.getQuestionContent();
        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));
        // 对问题数据进行校验
        questionService.validQuestion(question, true);
        // 获取当前登录用户，并将其ID设置到问题实体中
        User loginUser = userService.getLoginUser(request);
        question.setUserId(loginUser.getId());
        // 将问题实体保存到数据库
        boolean result = questionService.save(question);
        // 校验保存操作是否成功
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 获取新插入的问题ID并返回
        long newQuestionId = question.getId();
        return ResultUtils.success(newQuestionId);
    }

    /**
     * 删除题目
     *
     * @param deleteRequest 删除请求体，包含要删除的题目ID
     * @param request       HTTP请求对象，用于获取当前用户信息
     * @return 返回一个Boolean类型的BaseResponse对象，表示删除是否成功
     * @throws BusinessException 如果删除请求参数有误或用户无权限删除
     */
    @PostMapping("/delete")
    public BaseResponse<Boolean> deleteQuestion(@RequestBody DeleteRequest deleteRequest,
                                                HttpServletRequest request) {
        // 校验删除请求是否为空或ID是否有效
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 获取当前登录用户
        User user = userService.getLoginUser(request);
        // 获取待删除题目的ID
        long id = deleteRequest.getId();
        // 根据ID查询题目是否存在
        Question oldQuestion = questionService.getById(id);
        // 如果题目不存在，则抛出异常
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 判断当前用户是否有权限删除题目(只有题目的创建者或管理员才有权限删除)
        if (!oldQuestion.getUserId().equals(user.getId()) && userService.isAdmin(request)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 执行删除操作
        boolean result = questionService.removeById(id);
        // 如果删除操作失败，则抛出异常
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 返回删除成功的响应
        return ResultUtils.success(true);
    }

    /**
     * 更新题目（仅管理员可用）
     * 该方法接收一个更新题目请求对象，对题目信息进行更新
     * 仅允许管理员角色调用此接口
     *
     * @param questionUpdateRequest 更新题目请求对象，包含需要更新的题目信息
     * @return 返回一个基础响应对象，其中包含一个布尔值，表示更新是否成功
     */
    @PostMapping("/update")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> updateQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest) {
        // 校验入参是否有效
        if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 初始化一个新的 Question 对象，并将请求中的属性复制到该对象中
        Question question = new Question();
        BeanUtils.copyProperties(questionUpdateRequest, question);
        // 处理题目内容列表，将其转换为 JSON 字符串以便存储
        List<QuestionContentDTO> questionContentDTO = questionUpdateRequest.getQuestionContent();
        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));
        // 对题目数据进行校验，确保数据的完整性和合法性
        questionService.validQuestion(question, false);
        // 根据 ID 查询题目是否存在
        long id = questionUpdateRequest.getId();
        Question oldQuestion = questionService.getById(id);
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 更新数据库中的题目信息
        boolean result = questionService.updateById(question);
        // 如果更新失败，抛出异常
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 返回更新成功的响应
        return ResultUtils.success(true);
    }

    /**
     * 编辑题目（给用户使用）
     *
     * @param questionUpdateRequest 包含更新问题的信息的请求体
     * @param request               HTTP 请求对象，用于获取用户信息
     * @return 返回一个布尔值，表示更新是否成功
     */
    @PostMapping("/edit")
    public BaseResponse<Boolean> editQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest,
                                              HttpServletRequest request) {
        // 校验入参是否有效
        if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 初始化一个新的 Question 对象，并将请求体中的属性复制给它
        Question question = new Question();
        BeanUtils.copyProperties(questionUpdateRequest, question);
        // 处理问题内容列表，将其转换为 JSON 字符串以便存储
        List<QuestionContentDTO> questionContentDTO = questionUpdateRequest.getQuestionContent();
        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));
        // 对问题数据进行校验，确保数据的完整性和合法性
        questionService.validQuestion(question, false);
        // 获取当前登录的用户
        User loginUser = userService.getLoginUser(request);
        // 根据ID获取原有问题信息，以进行后续的权限校验和更新操作
        long id = questionUpdateRequest.getId();
        Question oldQuestion = questionService.getById(id);
        // 如果找不到原有问题，抛出异常
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 校验权限：只有问题的创建者或管理员才能编辑问题
        if (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 更新问题信息到数据库
        boolean result = questionService.updateById(question);
        // 如果更新失败，抛出异常
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 返回成功响应，包含更新结果
        return ResultUtils.success(true);
    }

    /**
     * 根据 id 获取题目（封装类）
     * 该方法通过接收一个题目ID来查询并返回一个封装的题目对象QuestionVO
     * 它首先验证ID的有效性，然后从数据库中检索对应的题目对象，
     * 如果题目存在，则使用该题目对象生成一个封装的QuestionVO对象，并通过BaseResponse返回
     *
     * @param id      题目ID，用于查询特定的题目
     * @return BaseResponse<QuestionVO> 包含封装的题目对象的响应结果
     */
    @GetMapping("/get/vo")
    public BaseResponse<QuestionVO> getQuestionVOById(long id) {
        // 检查ID是否有效，如果ID不合法，则抛出参数错误异常
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
        // 从数据库中查询指定ID的题目对象
        Question question = questionService.getById(id);
        // 如果查询结果为空，则抛出未找到错误异常
        ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR);
        // 获取封装后的题目对象并返回成功响应
        return ResultUtils.success(questionService.getQuestionVO(question));
    }

    /**
     * 分页获取题目列表（仅管理员可用）
     * 通过分页的方式获取题目列表，这种方式可以有效减少数据量大的情况下对服务器和数据库的压力
     * 只有具备管理员角色的用户才能访问此接口
     *
     * @param questionQueryRequest 请求体，包含分页参数和查询条件
     * @return 返回分页的题目列表，包括当前页的题目列表、总页数、总记录数等信息
     */
    @PostMapping("/list/page")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Page<Question>> listQuestionByPage(@RequestBody QuestionQueryRequest questionQueryRequest) {
        // 获取请求中的当前页码和每页显示数量
        long current = questionQueryRequest.getCurrent();
        long size = questionQueryRequest.getPageSize();
        // 使用MyBatis-Plus的Page和QueryWrapper进行分页查询
        // 构造分页对象并设置当前页和每页显示数量
        // 使用服务层的方法封装查询条件，实现动态SQL
        Page<Question> questionPage = questionService.page(new Page<>(current, size),
                questionService.getQueryWrapper(questionQueryRequest));
        // 返回成功的结果，包括分页后的题目列表
        return ResultUtils.success(questionPage);
    }

    /**
     * 分页获取题目列表（封装类）
     * 本方法通过分页的方式获取题目列表，同时对请求参数进行校验，
     * 防止爬虫行为，并返回封装后的题目信息。
     *
     * @param questionQueryRequest 题目查询请求封装类，包含分页信息和查询条件
     * @return 返回一个基础响应对象，包含分页的题目数据封装类
     */
    @PostMapping("/list/page/vo")
    public BaseResponse<Page<QuestionVO>> listQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest) {
        // 获取当前页码
        long current = questionQueryRequest.getCurrent();
        // 获取页面大小
        long size = questionQueryRequest.getPageSize();
        // 限制爬虫：如果页面大小超过设定值，则抛出异常
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 查询数据库：根据当前页码和页面大小，以及查询条件，获取题目分页数据
        Page<Question> questionPage = questionService.page(new Page<>(current, size),
                questionService.getQueryWrapper(questionQueryRequest));
        // 返回成功响应：包含封装后的题目数据
        return ResultUtils.success(questionService.getQuestionVOPage(questionPage));
    }

    /**
     * 分页获取当前登录用户创建的题目列表
     * 本方法通过分页的方式，根据当前登录用户的信息和其他查询条件，
     * 从数据库中获取用户创建的题目列表
     *
     * @param questionQueryRequest 题目查询请求对象，包含了分页信息和其他查询条件
     * @param request              HTTP请求对象，用于获取当前登录用户的信息
     * @return 返回一个基础响应对象，包含了分页的题目列表数据
     */
    @PostMapping("/my/list/page/vo")
    public BaseResponse<Page<QuestionVO>> listMyQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                                 HttpServletRequest request) {
        // 校验入参是否为空
        ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR);
        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);
        // 设置查询条件，只查询当前登录用户创建的题目
        questionQueryRequest.setUserId(loginUser.getId());
        // 获取当前页码和每页大小
        long current = questionQueryRequest.getCurrent();
        long size = questionQueryRequest.getPageSize();
        // 防止爬虫，限制每页大小最大为20
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 执行分页查询
        Page<Question> questionPage = questionService.page(new Page<>(current, size),
                questionService.getQueryWrapper(questionQueryRequest));
        // 封装并返回分页的题目列表数据
        return ResultUtils.success(questionService.getQuestionVOPage(questionPage));
    }

    private static final String GENERATE_QUESTION_SYSTEM_MESSAGE = """
            你是一位严谨的出题专家，我会给你如下信息：
            ```
            应用名称，
            【【【应用描述】】】，
            应用类别，
            要生成的题目数，
            每个题目的选项数
            ```

            请你根据上述信息，按照以下步骤来出题：
            1. 要求：题目和选项尽可能地短，题目不要包含序号，每题的选项数以我提供的为主，题目不能重复
            2. 严格按照下面的 json 格式输出题目和选项
            ```
            [{"options":[{"value":"选项内容","key":"A"},{"value":"","key":"B"}],"title":"题目标题"}]
            ```
            title 是题目，options 是选项，每个选项的 key 按照英文字母序（比如 A、B、C、D）以此类推，value 是选项内容
            3. 检查题目是否包含序号，若包含序号则去除序号
            4. 返回的题目列表格式必须为 JSON 数组""";

    /**
     * 生成题目的用户消息
     * 此方法用于构造一个格式化的用户消息字符串，包括应用名称、应用描述、应用类型、题目编号和选项编号
     *
     * @param app            应用程序对象，包含应用相关信息
     * @param questionNumber 题目的编号
     * @param optionNumber   选项的编号
     * @return 返回格式化后的用户消息字符串
     */
    private String getGenerateQuestionUserMessage(App app, int questionNumber, int optionNumber) {
        // 返回构建好的用户消息字符串
        return app.getAppName() + "\n" +
                // 添加应用描述到消息中
                app.getAppDesc() + "\n" +
                // 添加应用类型到消息中，通过AppTypeEnum的静态方法获取应用类型的文本描述
                Objects.requireNonNull(AppTypeEnum.getEnumByValue(app.getAppType())).getText() + "类" + "\n" +
                // 添加题目编号到消息中
                questionNumber + "\n" +
                // 添加选项编号到消息中
                optionNumber;
    }

    /**
     * AI生成题目接口
     *
     * @param aiGenerateQuestionRequest 生成题目请求体，包含appId，questionNumber，optionNumber
     * @return 返回生成的题目列表
     */
    @PostMapping("/ai_generate")
    public BaseResponse<List<QuestionContentDTO>> aiGenerateQuestion(
            @RequestBody AiGenerateQuestionRequest aiGenerateQuestionRequest) {
        // 校验请求参数是否为空
        ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);
        // 获取参数
        Long appId = aiGenerateQuestionRequest.getAppId();
        int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();
        int optionNumber = aiGenerateQuestionRequest.getOptionNumber();
        // 根据appId获取应用信息
        App app = appService.getById(appId);
        // 校验应用是否存在
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
        // 根据应用信息、题目数量和选项数量生成Prompt
        String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);
        // 向AI系统发送同步请求，生成题目
        String result = aiManager.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);
        // 截取需要的 JSON 信息
        int start = result.indexOf("[");
        int end = result.lastIndexOf("]");
        String json = result.substring(start, end + 1);
        // 将JSON字符串转换为题目DTO列表
        List<QuestionContentDTO> questionContentDTOList = JSONUtil.toList(json, QuestionContentDTO.class);
        // 返回生成的题目列表
        return ResultUtils.success(questionContentDTOList);
    }
}
